package demoMod.anm2editor.ui;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.Pixmap;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.math.Interpolation;
import com.badlogic.gdx.math.MathUtils;
import demoMod.anm2editor.enums.CustomCurveType;
import demoMod.anm2editor.fonts.FontKeys;
import demoMod.anm2editor.model.Anchor;
import demoMod.anm2editor.model.CustomInterpolation;
import demoMod.gdxform.abstracts.Container;
import demoMod.gdxform.helpers.FontHelper;
import demoMod.gdxform.interfaces.Element;
import demoMod.gdxform.interfaces.KeyboardEventSubscriber;
import demoMod.gdxform.interfaces.MouseEventSubscriber;

import java.util.ArrayList;
import java.util.List;

/**
 * 曲线函数图像展示
 */
public class CurveGraph extends Container<Element> implements MouseEventSubscriber, KeyboardEventSubscriber {
    /**
     * 绘制曲线时使用的纹理
     */
    private final Texture pencil;
    /**
     * 绘制锚点时使用的纹理
     */
    private final Texture anchorMark;
    /**
     * 绘制辅助点与锚点之间的连线
     */
    private final Texture guideLine;
    /**
     * 是否为自定义曲线，如果否，用户将不能编辑曲线
     */
    private boolean isCustom;
    /**
     * 是否允许用户编辑锚点，如果否，用户将不能编辑曲线，锚点信息也不会展示
     */
    private boolean canEditAnchors;

    /**
     * 是否显示辅助锚点，辅助锚点仅在曲线类型为钢笔时有效
     */
    private boolean showGuideAnchors;

    /**
     * libgdx自带的曲线，用户无法编辑
     */
    private Interpolation defaultCurve;
    /**
     * 自定义曲线
     */
    private CustomInterpolation customCurve;
    /**
     * 锚点在曲线图像上的展示
     */
    private final List<Point> points = new ArrayList<>();

    private Point currentSelectedAnchor;

    public CurveGraph() {
        setX(3);
        setY(3);
        setBackground(Color.WHITE.cpy());
        setBackground(Color.GRAY.cpy());
        setBorderWidth(3);
        setBorder(Color.WHITE.cpy());

        Pixmap pencilPixmap = new Pixmap(3, 3, Pixmap.Format.RGBA8888);
        pencilPixmap.setColor(Color.WHITE);
        pencilPixmap.fillCircle(1, 1, 2);

        pencil = new Texture(pencilPixmap);

        Pixmap anchorPixmap = new Pixmap(5, 5, Pixmap.Format.RGBA8888);
        anchorPixmap.setColor(Color.WHITE);
        anchorPixmap.drawRectangle(0, 0, 5, 5);

        anchorMark = new Texture(anchorPixmap);

        Pixmap guideLinePixmap = new Pixmap(1, 1, Pixmap.Format.RGBA8888);
        guideLinePixmap.setColor(Color.DARK_GRAY);
        guideLinePixmap.fill();

        guideLine = new Texture(guideLinePixmap);
    }

    @Override
    public void render(SpriteBatch sb) {
        getFrameBuffer().begin();
        sb.setColor(Color.WHITE);

        if (this.borderTexture != null) {
            sb.draw(this.borderTexture, getX(true) - getBorderWidth(), getY(true) - getBorderWidth(), getWidth() + getBorderWidth() * 2, getHeight() + getBorderWidth() * 2);
        }
        if (this.bgTexture != null) {
            sb.draw(this.bgTexture, getX(true), getY(true), getWidth(), getHeight());
        }

        float x = getX(true);
        Interpolation curve;
        if (isCustom) {
            curve = customCurve;
        } else {
            curve = defaultCurve;
        }
        while (x <= getX(true) + getWidth()) {
            float out = curve.apply((x - getX(true)) / getWidth());
            float y = getY(true) + getHeight() * out;
            float nextOut = curve.apply((x + 3.0F - getX(true)) / getWidth());
            float nextY = getY(true) + getHeight() * nextOut;
            sb.draw(pencil, x, y, 2, 2, (float) Math.sqrt(
                    9 + (nextY - y) * (nextY - y)
            ), 3, 1, 1, MathUtils.radiansToDegrees * MathUtils.atan2(nextY - y, 3), 0, 0, 3, 3, false, false);
            x += 3.0F;
        }

        if (isCustom && canEditAnchors) {
            int index = 1;
            FontHelper.getFont(FontKeys.SIM_HEI_12).setColor(Color.WHITE);
            for (Point point : points) {
                sb.setColor(point == currentSelectedAnchor ? Color.GREEN : Color.BLACK);
                float pointDrawX = getX(true) + point.x - 3.0F;
                float pointDrawY = getY(true) + point.y - 3.0F;
                sb.draw(anchorMark, pointDrawX, pointDrawY, 2, 2, 5, 5, 1, 1, 0, 0, 0, 5, 5, false, false);

                if (showGuideAnchors) {
                    for (Anchor guideAnchor : point.anchor.getBezierPoints()) {
                        float anchorX = guideAnchor.getX() * getWidth() + getX(true) - 1.5F;
                        float anchorY = guideAnchor.getY() * getHeight() + getY(true) - 1.5F;
                        sb.setColor(Color.BLACK);
                        sb.draw(anchorMark, anchorX, anchorY, 2, 2, 3, 3, 1, 1, 0, 0, 0, 5, 5, false, false);
                        sb.setColor(Color.WHITE);
                        sb.draw(guideLine, anchorX + 2, anchorY + 2, 0, 0, (float) Math.sqrt(
                                (anchorX - pointDrawX) * (anchorX - pointDrawX) + (anchorY - pointDrawY) * (anchorY - pointDrawY)
                        ), 1, 1, 1, MathUtils.radiansToDegrees * MathUtils.atan2(pointDrawY - anchorY, pointDrawX - anchorX), 0, 0, 1, 1, false, false);
                    }
                }

                FontHelper.getFont(FontKeys.SIM_HEI_12).draw(sb, Integer.toString(index), getX(true) + point.x, getY(true) + point.y);
                index++;
            }
        }

        sb.flush();
        getFrameBuffer().end();

        Texture texture = getFrameBuffer().getColorBufferTexture();
        sb.setColor(Color.WHITE);
        sb.draw(texture,
                getX(true) - this.getBorderWidth(), getY(true) - this.getBorderWidth(),
                0, 0,
                getWidth() + this.getBorderWidth() * 2, getHeight() + this.getBorderWidth() * 2,
                1, 1,
                0,
                (int) getX(true) - (int) this.getBorderWidth(), (int) getY(true) - (int) this.getBorderWidth(),
                (int) getWidth() + (int) this.getBorderWidth() * 2, (int) getHeight() + (int) this.getBorderWidth() * 2,
                false, true);
        sb.flush();
    }

    public void setCustom(boolean custom) {
        isCustom = custom;
    }

    public boolean canEditAnchors() {
        return canEditAnchors;
    }

    public void setCanEditAnchors(boolean canEditAnchors) {
        this.canEditAnchors = canEditAnchors;
    }

    public void setCustomCurveType(CustomCurveType curveType) {
        this.customCurve.setCurveType(curveType);
        this.showGuideAnchors = curveType == CustomCurveType.PEN;
    }

    public CustomInterpolation getCustomCurve() {
        return customCurve;
    }

    public void setCustomCurve(CustomInterpolation customCurve) {
        this.customCurve = customCurve;
        this.showGuideAnchors = customCurve.getCurveType() == CustomCurveType.PEN;
        refreshPoints();
    }

    public Interpolation getDefaultCurve() {
        return defaultCurve;
    }

    public void addAnchor(Anchor anchor) {
        this.customCurve.addAnchor(anchor);
        refreshPoints();
    }

    public void deleteAnchor() {
        if (currentSelectedAnchor != null) {
            this.customCurve.removeAnchor(currentSelectedAnchor.anchor);
            currentSelectedAnchor = null;
            refreshPoints();
        }
    }

    public Anchor getCurrentSelectedAnchor() {
        if (currentSelectedAnchor == null) {
            return null;
        }
        return currentSelectedAnchor.anchor;
    }

    public void setCurrentSelectedAnchor(Anchor currentSelectedAnchor) {
        for (Point point : points) {
            if (point.anchor == currentSelectedAnchor) {
                this.currentSelectedAnchor = point;
                break;
            }
        }
    }

    public void refreshPoint(Anchor anchor) {
        for (Point point : points) {
            if (point.anchor == anchor) {
                point.x = anchor.getX() * getWidth();
                point.y = anchor.getY() * getHeight();
                break;
            }
        }
    }

    private void refreshPoints() {
        this.points.clear();
        for (Anchor anchor : customCurve.getAnchors()) {
            Point point = new Point();
            point.x = anchor.getX() * getWidth();
            point.y = anchor.getY() * getHeight();
            point.anchor = anchor;
            points.add(point);
        }
    }

    public void setDefaultCurve(Interpolation defaultCurve) {
        this.defaultCurve = defaultCurve;
    }

    @Override
    public boolean keyDown(int keyCode) {
        return false;
    }

    @Override
    public boolean keyUp(int keyCode) {
        return false;
    }

    @Override
    public boolean keyTyped(char key) {
        return false;
    }



    @Override
    public boolean clickDown(int screenX, int screenY, int button) {
        boolean mouseDown = (float)screenX >= this.getX(true) &&
                (float)screenX <= this.getX(true) + this.getWidth() &&
                (float)(Gdx.graphics.getHeight() - screenY) >= this.getY(true) &&
                (float)(Gdx.graphics.getHeight() - screenY) <= this.getY(true) + this.getHeight();
        if (mouseDown) {
            float relX = screenX - this.getX(true);
            float relY = (float)(Gdx.graphics.getHeight() - screenY) - this.getY(true);
            for (Point point : points) {
                if (Math.abs(point.x - relX) <= 5 && Math.abs(point.y - relY) <= 5) {
                    setCurrentSelectedAnchor(point.anchor);
                    break;
                }
            }
            return true;
        }
        return false;
    }

    @Override
    public boolean clickUp(int screenX, int screenY, int button) {
        startDragging = false;
        startDraggingGuide = false;
        return false;
    }

    private boolean startDragging = false;
    private boolean startDraggingGuide = false;
    private int draggingGuideIndex = 0;

    @Override
    public boolean mouseDragged(int screenX, int screenY) {
        boolean mouseDown = (float)screenX >= this.getX(true) &&
                (float)screenX <= this.getX(true) + this.getWidth() &&
                (float)(Gdx.graphics.getHeight() - screenY) >= this.getY(true) &&
                (float)(Gdx.graphics.getHeight() - screenY) <= this.getY(true) + this.getHeight();
        if (mouseDown && isCustom && canEditAnchors) {
            if (currentSelectedAnchor != null) {
                float relX = screenX - this.getX(true);
                float relY = (float)(Gdx.graphics.getHeight() - screenY) - this.getY(true);
                if (startDragging) {
                    currentSelectedAnchor.x = relX;
                    currentSelectedAnchor.y = relY;

                    float originalX = currentSelectedAnchor.anchor.getX();
                    float originalY = currentSelectedAnchor.anchor.getY();

                    currentSelectedAnchor.anchor.setX(relX / getWidth());
                    currentSelectedAnchor.anchor.setY(relY / getHeight());
                    if (currentSelectedAnchor.anchor == customCurve.getAnchors().get(0)) {
                        currentSelectedAnchor.x = 0.0F;
                        currentSelectedAnchor.anchor.setX(0.0F);
                    } else if (currentSelectedAnchor.anchor == customCurve.getAnchors().get(customCurve.getAnchors().size() - 1)) {
                        currentSelectedAnchor.x = getWidth();
                        currentSelectedAnchor.anchor.setX(1.0F);
                    }
                    for (Anchor guideAnchor : currentSelectedAnchor.anchor.getBezierPoints()) {
                        guideAnchor.setX(guideAnchor.getX() + (currentSelectedAnchor.anchor.getX() - originalX));
                        guideAnchor.setY(guideAnchor.getY() + (currentSelectedAnchor.anchor.getY() - originalY));
                    }
                }
                if (startDraggingGuide && !startDragging) { //防止冲突，避免锚点和辅助点被一起拖动导致无法分离锚点和辅助点
                    Anchor guideAnchor = currentSelectedAnchor.anchor.getBezierPoints().get(draggingGuideIndex);
                    guideAnchor.setX(relX / getWidth());
                    guideAnchor.setY(relY / getHeight());
                }
                if (Math.abs(currentSelectedAnchor.x - relX) <= 5 && Math.abs(currentSelectedAnchor.y - relY) <= 5 && !startDragging) {
                    startDragging = true;
                }
                for (Anchor guideAnchor : currentSelectedAnchor.anchor.getBezierPoints()) {
                    if (Math.abs(guideAnchor.getX() * getWidth() - relX) <= 3 && Math.abs(guideAnchor.getY() * getHeight() - relY) <= 3 && !startDraggingGuide) {
                        startDraggingGuide = true;
                        draggingGuideIndex = currentSelectedAnchor.anchor.getBezierPoints().indexOf(guideAnchor);
                        break;
                    }
                }
            }
            return true;
        }
        return false;
    }

    @Override
    public boolean mouseMoved(int screenX, int screenY) {
        return false;
    }

    @Override
    public boolean scrolled(int amount) {
        return false;
    }

    @Override
    public boolean moveToElementBorder(Element element) {
        return false;
    }

    @Override
    public void dispose() {
        super.dispose();
        if (pencil != null) {
            pencil.dispose();
        }
        if (anchorMark != null) {
            anchorMark.dispose();
        }
        if (guideLine != null) {
            guideLine.dispose();
        }
    }

    static class Point {
        float x;
        float y;
        Anchor anchor;
    }
}
